﻿// HSS.Forms.AppData.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.html
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2010
// ----------------------------------------------------------------------------
// File:       AppData.cs
// Author:     HSS\gbanta
// Created:    08/12/2010
// Modified:   12/04/2010
// ----------------------------------------------------------------------------
namespace HSS.Forms
{
	#region Using Directives
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.ComponentModel;
	using System.IO;
	using System.Text;
	using System.Xml;
	using System.Linq;
	#endregion

	#region AppData
	/// <summary>
	/// Provide the ability to store Application Data and persist to/from a file.
	/// </summary>
	public class AppData
	{
		#region Static Methods
		/// <summary>
		/// Creates a new AppData instance from the Attributes.
		/// </summary>
		/// <param name="reader">The XmlReader containing the Attributes to be read.</param>
		/// <returns>The instantiated AppData</returns>
		public static AppData FromAttributes(XmlReader reader)
		{
			AppData appData = new AppData();
			if (reader.HasAttributes)
			{
				for (int i = 0; i < reader.AttributeCount; i++)
				{
					reader.MoveToAttribute(i);
					appData[reader.Name] = reader.Value;
				}
				reader.MoveToElement(); //Moves the reader back to the element node.
			}
			return appData;
		}
		/// <summary>
		/// Creates a new AppData instance from a File.
		/// </summary>
		/// <param name="fileName">The file to load.</param>
		/// <returns>The instantiated AppData</returns>
		public static AppData FromFile(string fileName)
		{
			if (!File.Exists(fileName))
				return null;

			using (XmlTextReader reader = new XmlTextReader(fileName))
			{
				while (reader.Read())
				{
					if (reader.IsStartElement())
					{
						switch (reader.LocalName)
						{
							case "AppData":
								AppData appData = new AppData();
								appData.ReadAll(reader, "AppData");
								return appData;
						}
					}
				}
			}
			return null;
		}
		#endregion

		#region Events
		/// <summary>
		/// An event to notify listeners of when the data has changed.
		/// </summary>
		public event EventHandler<AppDataChangedEventArgs> AppDataChanged;
		/// <summary>
		/// An event to notify listeners of when the data is about to be changed.
		/// </summary>
		public event EventHandler<AppDataChangingEventArgs> AppDataChanging;
		/// <summary>
		/// Fires the AppDataChanged Event
		/// </summary>
		/// <param name="e">An instance of the AppDataChangedArgs</param>
		protected virtual void OnChanged(AppDataChangedEventArgs e)
		{
			if (AppDataChanged != null)
				AppDataChanged(this, e);
		}
		/// <summary>
		/// Raises the Changing event.</summary>
		/// <param name="e">
		/// The arguments object associated with the Changing event.</param>
		/// <remarks>
		/// This method should be invoked prior to making a change to the data so that the
		/// Changing event is raised, giving a chance to the handlers to prevent the change from
		/// happening (by setting e.Cancel to true). This method calls each individual handler 
		/// associated with the Changing event and checks the resulting e.Cancel flag. 
		/// If it's true, it stops and does not call any of the remaining handlers since the change 
		/// needs to be prevented anyway.</remarks>
		protected virtual void OnChanging(AppDataChangingEventArgs e)
		{
			if (AppDataChanging != null)
			{
				foreach (EventHandler<AppDataChangingEventArgs> handler in AppDataChanging.GetInvocationList())
				{
					handler(this, e);
					if (e.Cancel)
						return;
				}
			}
		}
		/// <summary>
		/// Raises either the Changing or Changed event.
		/// </summary>
		/// <param name="changing"></param>
		/// <param name="key">The key used to reference the AppData</param>
		/// <param name="oldValue">The old value</param>
		/// <param name="newValue">The new value</param>
		/// <returns>
		/// The return value is based on the event raised. If the Changing event was raised, 
		/// the return value is the opposite of AppDataChangingArgs.Cancel; otherwise it's true.</returns>
		/// <remarks>
		/// This method may be used by derived classes as a convenient alternative to calling 
		/// OnChanging and OnChanged. For example, a typical call to OnChanging would require
		/// four lines of code, which this method reduces to two.</remarks>
		private bool RaiseChangeEvent(bool changing, string key, object oldValue, object newValue)
		{
			if (changing)
			{
				if (AppDataChanging == null)
					return true;
				AppDataChangingEventArgs e = new AppDataChangingEventArgs(this, key, oldValue, newValue);
				OnChanging(e);
				return !e.Cancel;
			}
			if (AppDataChanged != null)
				OnChanged(new AppDataChangedEventArgs(this, key, oldValue, newValue));
			return true;
		}
		#endregion

		#region Fields
		Dictionary<string, object> items = new Dictionary<string, object>();
		#endregion

		#region Constructor
		/// <summary>
		/// Default Constructor
		/// </summary>
		public AppData()
		{
		}
		#endregion

		#region Properties
		/// <summary>
		/// Index access to the elements (values)
		/// </summary>
		/// <param name="key">The key of the appData to return</param>
		/// <returns>The value</returns>
		public object this[string key]
		{
			get { return Get(key); }
			set { Set(key, value); }
		}
		/// <summary>
		/// An ReadOnlyCollection of the keys contained in this instance
		/// </summary>
		public ReadOnlyCollection<string> Keys
		{
			get { return items.Keys.ToList().AsReadOnly(); }
		}
		/// <summary>
		/// Gets the count of elements contained in this key
		/// </summary>
		public int Count
		{
			get { return items.Count; }
		}
		#endregion

		#region Methods
		/// <summary>
		/// Gets an Application Data Value
		/// </summary>
		/// <param name="key">The key to locate the value by</param>
		/// <returns>A object containing the value requested</returns>
		public object Get(string key)
		{
			if (!items.ContainsKey(key))
				return null;
			return items[key];
		}
		/// <summary>
		/// Gets an Application Data Value
		/// </summary>
		/// <typeparam name="T">The TypeOf value being retrieved</typeparam>
		/// <param name="key">The key to locate the value by</param>
		/// <param name="defaultValue">The default value to store</param>
		/// <returns>An object of the Type specified by T, containing the value</returns>
		public T Get<T>(string key, T defaultValue)
		{
			if (!items.ContainsKey(key))
			{
				if (null != defaultValue)
					items.Add(key, defaultValue);
				return defaultValue;
			}

			object o = items[key];

			if (o is string && typeof(T) != typeof(string))
			{
				TypeConverter c = TypeDescriptor.GetConverter(typeof(T));
				try { o = c.ConvertFromInvariantString(o.ToString()); }
				catch (Exception ex)
				{
					MessageService.ShowWarning("Error loading appData '" + key + "': " + ex.Message);
					o = defaultValue;
				}
				items[key] = o; // store for future look up
			}
			else if (o is ArrayList)
			{
				ArrayList list = (ArrayList)o;
				Type elementType = typeof(T).GetElementType();
				Array arr = System.Array.CreateInstance(elementType, list.Count);
				TypeConverter c = TypeDescriptor.GetConverter(elementType);
				try
				{
					for (int i = 0; i < arr.Length; ++i)
					{
						if (list[i] != null)
							arr.SetValue(c.ConvertFromInvariantString(list[i].ToString()), i);
					}
					o = arr;
				}
				catch (Exception ex)
				{
					MessageService.ShowWarning("Error loading appData '" + key + "': " + ex.Message);
					o = defaultValue;
				}
				items[key] = o; // store for future look up
			}
			else if (!(o is string) && typeof(T) == typeof(string))
			{
				TypeConverter c = TypeDescriptor.GetConverter(typeof(T));
				if (c.CanConvertTo(typeof(string)))
					o = c.ConvertToInvariantString(o);
				else
					o = o.ToString();
			}

			try { return (T)o; }
			catch (NullReferenceException)
			{
				// can happen when configuration is invalid -> o is null and a value type is expected
				return defaultValue;
			}
		}
		/// <summary>
		/// Sets an Application Data Value
		/// </summary>
		/// <typeparam name="T">The TypeOf value being stored</typeparam>
		/// <param name="key">The key to reference the value by</param>
		/// <param name="value">The value to store</param>
		public void Set<T>(string key, T value)
		{
			T oldValue = default(T);
			if (!items.ContainsKey(key))
				items.Add(key, value);
			else
			{
				oldValue = Get<T>(key, value);
				if (this.RaiseChangeEvent(true, key, oldValue, value))
					items[key] = value;
			}
			this.RaiseChangeEvent(false, key, oldValue, value);
		}
		/// <summary>
		/// Read all Application Data items from an XmlReader instance
		/// </summary>
		/// <param name="reader">The XmlReader containing the items to load</param>
		/// <param name="endElement">The name of the last element for the appData</param>
		public void ReadAll(XmlReader reader, string endElement)
		{
			if (reader.IsEmptyElement)
				return;

			while (reader.Read())
			{
				switch (reader.NodeType)
				{
					case XmlNodeType.EndElement:
						if (reader.LocalName == endElement)
							return;
						break;

					case XmlNodeType.Element:
						string key = reader.LocalName;
						if (key == "AppData")
						{
							key = reader.GetAttribute(0);
							AppData p = new AppData();
							p.ReadAll(reader, "AppData");
							items[key] = p;
						}
						else if (key == "Array")
						{
							key = reader.GetAttribute(0);
							items[key] = ReadArray(reader);
						}
						else
						{
							items[key] = reader.HasAttributes ? reader.GetAttribute(0) : null;
						}
						break;
				}
			}
		}
		/// <summary>
		/// Write all the current Application Data to an XmlWriter
		/// </summary>
		/// <param name="writer">The XmlWriter to store all the Application Data</param>
		public void WriteAll(XmlWriter writer)
		{
			foreach (KeyValuePair<string, object> entry in items)
			{
				object val = entry.Value;
				if (val is AppData)
				{
					writer.WriteStartElement("AppData");
					writer.WriteAttributeString("name", entry.Key);
					((AppData)val).WriteAll(writer);
					writer.WriteEndElement();
				}
				else if (val is IList)
				{
					writer.WriteStartElement("Array");
					writer.WriteAttributeString("name", entry.Key);
					foreach (object o in (IEnumerable)val)
					{
						writer.WriteStartElement("Element");
						WriteValue(writer, o);
						writer.WriteEndElement();
					}
					writer.WriteEndElement();
				}
				else
				{
					writer.WriteStartElement(entry.Key);
					WriteValue(writer, val);
					writer.WriteEndElement();
				}
			}
		}
		/// <summary>
		/// Persist the Application Data to file
		/// </summary>
		/// <param name="fileName">The file name to persist the Application Data to</param>
		public void Save(string fileName)
		{
			using (XmlTextWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
			{
				writer.Formatting = Formatting.Indented;
				writer.WriteStartElement("AppData");
				WriteAll(writer);
				writer.WriteEndElement();
			}
		}
		/// <summary>
		/// Determines whether the specified Application Data exist
		/// </summary>
		/// <param name="key">The name of the appData to locate</param>
		/// <returns>true if a appData with the specified key exist; otherwise, false.</returns>
		public bool Contains(string key)
		{
			return items.ContainsKey(key);
		}
		/// <summary>
		/// Remove the Application Data of the specified key
		/// </summary>
		/// <param name="key">The key of the Application Data to remove</param>
		/// <returns>true if the element is successfully found and removed; otherwise, false.</returns>
		public bool Remove(string key)
		{
			return items.Remove(key);
		}
		/// <summary>
		/// Converts the value of this instance to a System.String
		/// </summary>
		/// <returns>A string containing all the Application Data as a key value list</returns>
		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append("[AppData:{");
			foreach (KeyValuePair<string, object> entry in items)
			{
				sb.Append(entry.Key);
				sb.Append("=");
				sb.Append(entry.Value);
				sb.Append(",");
			}
			sb.Append("}]");
			return sb.ToString();
		}

		static void WriteValue(XmlWriter writer, object val)
		{
			if (val != null)
			{
				if (val is string)
					writer.WriteAttributeString("value", val.ToString());
				else
				{
					TypeConverter c = TypeDescriptor.GetConverter(val.GetType());
					writer.WriteAttributeString("value", c.ConvertToInvariantString(val));
				}
			}
		}

		static ArrayList ReadArray(XmlReader reader)
		{
			if (reader.IsEmptyElement)
				return new ArrayList(0);

			ArrayList l = new ArrayList();
			while (reader.Read())
			{
				switch (reader.NodeType)
				{
					case XmlNodeType.EndElement:
						if (reader.LocalName == "Array")
							return l;
						break;
					case XmlNodeType.Element:
						l.Add(reader.HasAttributes ? reader.GetAttribute(0) : null);
						break;
				}
			}
			return l;
		}

		#endregion
	}
	#endregion
}